/*
 * MIT License
 *
 * Copyright (c) 2020 wen.gu <454727014@qq.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

/***************************************************************************
 * Name: xsocket.cpp
 *
 * Purpose: wrapper socket operation API for linux platform
 *
 * Developer:
 *   wen.gu , 2023-07-12
 *
 * TODO:
 *
 ***************************************************************************/
#include "shmipc/com/internal/xsocket.h"

#include <sys/socket.h>
#include <sys/types.h>
#include <sys/un.h>
#include <unistd.h>
#include <cstring>
#include <errno.h>
#include <arpa/inet.h>

#define SOCK_SEND_TRY_MAX_CNT 10

namespace shmipc::com::internal {
using namespace shmipc::core;


struct XSocketHandler {
    XSocketType sock_type; 
    XSocketId sock_id;
    std::string url;
    XSocketHandler(XSocketType type, XSocketId fd)
    :sock_type(type),
    sock_id(fd) { /**Todo something */  }
    ~XSocketHandler() {
        if ((sock_type == XSocketType::UDS) && !url.empty()) {
            printf("[%s.%d]====>:unlink: %s\n", __FUNCTION__, __LINE__, url.c_str());
            unlink(url.c_str());
        }

        if (sock_id != INVALID_XSOCKET_ID) {
            shutdown(sock_id, SHUT_RDWR);
            ::close(sock_id); 
            sock_id = INVALID_XSOCKET_ID;               
        }    
    }
};

static inline struct sockaddr_in Url2Addr(const std::string& url, int32_t port) {
    struct sockaddr_in addr;
    addr.sin_addr.s_addr = inet_addr(url.c_str());
    addr.sin_family = AF_INET; 
    addr.sin_port = htons(port);

    return addr;
}

static inline int GetSocketType(XSocketType xtype) {
    switch (xtype)
    {
    case XSocketType::UDS:
    case XSocketType::TCP:
        return SOCK_STREAM;
        break;    
    default:
        break;
    }

    return SOCK_DGRAM;
}

static inline int GetSocketDomain(XSocketType xtype) {
    return (xtype == XSocketType::UDS) ? AF_UNIX : AF_INET;
}

static int SelectRecvPoll(XSocketId sock_id, fd_set& rd_set, int32_t timeout_ms) {
    FD_ZERO(&rd_set);
    FD_SET(sock_id, &rd_set);
    struct timeval sel_timeout{0};
    struct timeval* tv_ptr = nullptr;
    if (timeout_ms >= 0) {
        sel_timeout.tv_sec = timeout_ms / 1000;
        sel_timeout.tv_usec = (timeout_ms == core::WaitType::NoWait) ? 1 : ((timeout_ms % 1000) * 1000); /** set 1 to avoid cpu loading increase */
        tv_ptr = &sel_timeout;
    }

    return select(sock_id + 1, &rd_set, nullptr, nullptr, tv_ptr);
}

/** for client side */
static core::XErrC XSocketConnect(XSocketHandler* xsh, const std::string& url, int32_t port) {
    //printf("[%s.%d]====>:\n", __FUNCTION__, __LINE__);
    if (xsh->sock_type == XSocketType::UDS) {        
        struct sockaddr_un client_addr;
        client_addr.sun_family = AF_UNIX;
        snprintf(client_addr.sun_path, sizeof(client_addr.sun_path), "%s.client.%d.%ld.%p", url.c_str(), getpid(), xsh->sock_id, xsh);
        unlink(client_addr.sun_path);
        if (::bind(xsh->sock_id, (struct sockaddr*)&client_addr, sizeof(client_addr)) < 0) {
            //printf ("bind client url failed(err: %s)\n", strerror(errno));
            return XErrC::BindFailed;
        }
        
        struct sockaddr_un server_addr;
        server_addr.sun_family = AF_UNIX;
        strcpy(server_addr.sun_path, url.c_str());

        if (::connect(xsh->sock_id, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
            //printf ("connect to url(%s) failed(err: %s)\n", url.c_str(), strerror(errno));
            unlink(client_addr.sun_path);
            return XErrC::ConnectFailed;
        }  
        xsh->url = client_addr.sun_path;      
    } else {
        struct sockaddr_in addr = Url2Addr(url, port);
        if (::connect(xsh->sock_id, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
            printf ("connect to url(%s: %d) failed(err: %s)\n", url.c_str(), port, strerror(errno));
            return XErrC::ConnectFailed;
        }          
    }
    
    return XErrC::OK; 
}

/** for server side */
static core::XErrC XSocketBind(XSocketHandler* xsh, const std::string& url, int32_t port) {
   if (xsh->sock_type  == XSocketType::UDS) {
        struct sockaddr_un server_addr;
        server_addr.sun_family = AF_UNIX;
        strcpy(server_addr.sun_path, url.c_str());

        unlink(server_addr.sun_path); //todo refine me??
        if (::bind(xsh->sock_id, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
            printf ("bind server url(%s) failed(err: %s)\n", url.c_str(), strerror(errno));
            return XErrC::BindFailed;
        }

        if (::listen(xsh->sock_id, 10) < 0) {//todo refine me?
            printf("server(%s) listen for connections from client failed(err:%s)\n", url.c_str(), strerror(errno));
            unlink(server_addr.sun_path);
            return XErrC::ListenFailed;        
        } 

        xsh->url = url;      
    } else {
#if 1
        struct sockaddr_in addr = Url2Addr(url, port);
#else
        struct sockaddr_in addr;
        addr.sin_family = AF_INET; 
        addr.sin_port = htons(port);
        addr.sin_addr.s_addr = inet_addr(url.c_str());

#endif

        if (::bind(xsh->sock_id, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
            printf ("bind server url(%s:%d) failed(err: %s), sockt_id:%ld\n", url.c_str(), port, strerror(errno), xsh->sock_id);
            return XErrC::BindFailed;
        } 

        if (xsh->sock_type == XSocketType::TCP) {
            if (::listen(xsh->sock_id, 10) < 0) {//todo refine me?
                printf("server(%s) listen for connections from client failed(err:%s)\n", url.c_str(), strerror(errno));
                return XErrC::ListenFailed;        
            }             
        }     
    }

    return XErrC::OK;
}

static XResult<uint32_t> StreamRecv(XSocketHandler* xsh, uint8_t* buf, uint32_t size, int32_t timeout_ms) {
    fd_set rd_set;
    while (SelectRecvPoll(xsh->sock_id, rd_set, timeout_ms) > 0) {
        if ((FD_ISSET(xsh->sock_id, &rd_set))) {
            int32_t ret = ::recv(xsh->sock_id, buf, size, 0);
            if (ret > 0) {
                return {ret};
            } else if (ret < 0) {
                if ((errno == EAGAIN) || (errno == EWOULDBLOCK) || (errno == EINTR)) {
                    continue;
                }
                XResult<uint32_t>::FromError(XErrC::Undefined);
            } else {
                return XResult<uint32_t>::FromError(XErrC::Disconnected);
            }
        } //todo, if waitforever, is need try again?
    }

    return XResult<uint32_t>::FromError(XErrC::TimeOut);
}

static XResult<uint32_t> UdpRecv(XSocketHandler* xsh, XSocketId& src_fd, uint8_t* buf, uint32_t size, int32_t timeout_ms) {
    struct timeval sel_timeout{0};
    struct timeval* tv_ptr = nullptr;
    if (timeout_ms >= 0) {
        sel_timeout.tv_sec = timeout_ms / 1000;
        sel_timeout.tv_usec = (timeout_ms == core::WaitType::NoWait) ? 1 : ((timeout_ms % 1000) * 1000); /** set 1 to avoid cpu loading increase */
        tv_ptr = &sel_timeout;
    }

    while (setsockopt(xsh->sock_id, SOL_SOCKET, SO_RCVTIMEO, tv_ptr, sizeof(sel_timeout)) == 0) {
        struct sockaddr_in src_addr;
        socklen_t src_size = sizeof(src_addr);
        int32_t rsize = ::recvfrom(xsh->sock_id, buf, size, 0, (struct sockaddr *)&src_addr, &src_size);

        if (rsize > 0) {
            src_fd = *((XSocketId*)&src_addr);
            return {rsize};
        } else if ( rsize == 0) {
            return XResult<uint32_t>::FromError(XErrC::Disconnected);
        } else {
            if (errno == ETIMEDOUT) {
                return XResult<uint32_t>::FromError(XErrC::TimeOut);
            } else if ((errno == EAGAIN) || (errno == EWOULDBLOCK) || (errno == EINTR)) {
                continue;
            }
            return XResult<uint32_t>::FromError(XErrC::Undefined);
        }
    }

    return XResult<uint32_t>::FromError(XErrC::TimeOut);
}


static XResultCode StreamSend(int sock_id, const uint8_t* buf, uint32_t size) {
    uint32_t send_pos = 0;
    uint32_t max_try_cnt = SOCK_SEND_TRY_MAX_CNT;
    while (send_pos < size) { /** todo, spilt message if too big */
        int32_t ret = ::send(sock_id, buf + send_pos, size - send_pos, 0);
        if (ret <= 0) {
            if (ret < 0) {
                if ((errno == EAGAIN) || (errno == EWOULDBLOCK)) { 
                    return XResultCode::FromError(XErrC::Disconnected); /** todo, return right error code */ 
                }

                max_try_cnt--;
                if (max_try_cnt == 0) { 
                    return XResultCode::FromError(XErrC::Disconnected);  /** todo, return right error code */  
                }
                continue; //try again
            } else if (errno == EINTR){
                max_try_cnt = SOCK_SEND_TRY_MAX_CNT;
                continue;
            } else { return XResultCode::FromError(XErrC::Disconnected); }
        }
        max_try_cnt = SOCK_SEND_TRY_MAX_CNT;
        send_pos += ret;
    } 

    return { }; 
}

static XResultCode UdpSend(int sock_id, const struct sockaddr_in& dst_addr, const uint8_t* buf, uint32_t size) {
    uint32_t send_pos = 0;
    uint32_t max_try_cnt = SOCK_SEND_TRY_MAX_CNT;
    while (send_pos < size) { /** todo, spilt message if too big */
        int32_t ret = ::sendto(sock_id, buf + send_pos, size - send_pos, 0, (const struct sockaddr *)&dst_addr, sizeof(dst_addr));
        if (ret <= 0) {
            if (ret < 0) {
                if ((errno == EAGAIN) || (errno == EWOULDBLOCK)) { 
                    return XResultCode::FromError(XErrC::Disconnected); /** todo, return right error code */ 
                }

                max_try_cnt--;
                if (max_try_cnt == 0) { 
                    return XResultCode::FromError(XErrC::Disconnected);  /** todo, return right error code */  
                }
                continue; //try again
            } else if (errno == EINTR){
                max_try_cnt = SOCK_SEND_TRY_MAX_CNT;
                continue;
            } else { return XResultCode::FromError(XErrC::Disconnected); }
        }
        max_try_cnt = SOCK_SEND_TRY_MAX_CNT;
        send_pos += ret;
    } 

    return { };
}

static XSocketHandler* CreateSockHandler(XSocketType type) {
    XSocketId socket_fd = ::socket(GetSocketDomain(type), GetSocketType(type), 0);

    if (socket_fd < 0) {
        printf("open socket(type:%d) failed(%s)\n", static_cast<int>(type), strerror(errno));
        return nullptr;
    }

    XSocketHandler* xsh = new XSocketHandler(type, socket_fd);
    if (xsh) { return xsh; }
    ::close(socket_fd);
    return nullptr;
}

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

XResult<XSocketId> XSocketServerOpen(XSocketType type, const std::string& url, int32_t port /*= 0*/) {
    XSocketHandler* xsh = CreateSockHandler(type);

    if (!xsh) { return XResult<XSocketId>::FromError(XErrC::InsufficientReousces); }

    XErrC ret = XSocketBind(xsh, url, port);

    if (ret != XErrC::OK) {
        delete xsh;
        return XResult<XSocketId>::FromError(ret);
    }

    return reinterpret_cast<XSocketId>(xsh);
}

core::XResult<XSocketId> XSocketClientOpen(XSocketType type, const std::string& url, int32_t port /*= 0*/) {
    XSocketHandler* xsh = CreateSockHandler(type);

    if (!xsh) { return XResult<XSocketId>::FromError(XErrC::InsufficientReousces);  }

    XErrC ret = XSocketConnect(xsh, url, port);

    if (ret != XErrC::OK) {
        delete xsh;
        return XResult<XSocketId>::FromError(ret);
    }

    return reinterpret_cast<XSocketId>(xsh);
}

XResultCode XSocketClose(XSocketId sock_id) {
    XSocketHandler* xsh;
    if ((sock_id == INVALID_XSOCKET_ID) || !(xsh = reinterpret_cast<XSocketHandler*>(sock_id))) {
        return XResultCode::FromError(XErrC::InvalidArgument);
    } 

    delete xsh;

    return { };
}


/** for orainted-connection protocol(e.g. tcp, uds) */
XResult<XSocketId> XSocketAccept(XSocketId sock_id, int32_t timeout_ms /*= core::WaitType::WaitForever*/) { 
   if ((sock_id == INVALID_XSOCKET_ID)) {
        return XResult<XSocketId>::FromError(XErrC::InvalidArgument);
    }

    XSocketHandler* xsh = reinterpret_cast<XSocketHandler*>(sock_id);
    if (!xsh || (xsh->sock_type == XSocketType::UDP)) {
        return XResult<XSocketId>::FromError(XErrC::InvalidArgument);
    }

    XSocketId client_fd = INVALID_XSOCKET_ID;
    fd_set read_set;
    int32_t ret = SelectRecvPoll(xsh->sock_id, read_set, timeout_ms);

    if ((ret > 0) && FD_ISSET(xsh->sock_id, &read_set)) {
        socklen_t cli_size;
        std::string clien_url;
        if (xsh->sock_type == XSocketType::UDS) {            
            struct sockaddr_un cli;
            /* event from TCP server socket, new connection */
            cli_size = sizeof(cli);
            client_fd = ::accept(xsh->sock_id, (struct sockaddr *)&cli, &cli_size);  
            clien_url = cli.sun_path;
        } else {
            struct sockaddr_in client_addr;
            cli_size = sizeof(client_addr);
            client_fd = ::accept(xsh->sock_id, (struct sockaddr *)&client_addr, &cli_size);
            clien_url = std::string(inet_ntoa(client_addr.sin_addr)) + ":" + std::to_string(client_addr.sin_port);
        }

        if (client_fd >= 0) {
            printf("server(%s) got a connection from client(%s) with client id(%ld)\n", xsh->url.c_str(), clien_url.c_str(), client_fd);  
            XSocketHandler* clish = new XSocketHandler(xsh->sock_type, client_fd);  

            return reinterpret_cast<XSocketId>(clish);
        } else {
            printf("accept() for server(%s) failed: %s\n", xsh->url.c_str(), strerror(errno));
        } 
    }        

    return XResult<XSocketId>::FromError(XErrC::TimeOut);
}

XResult<uint32_t> XSocketRecv(XSocketId sock_id, uint8_t* buf, uint32_t size, int32_t timeout_ms /*= core::WaitType::WaitForever*/) {
    XSocketHandler* xsh;
    if ((sock_id == INVALID_XSOCKET_ID) || !(xsh = reinterpret_cast<XSocketHandler*>(sock_id))) {
        return XResult<uint32_t>::FromError(XErrC::InvalidArgument);
    }

    return StreamRecv(xsh, buf, size, timeout_ms);
}

/** dst_fd: is valid only for udp protocol */
XResultCode XSocketSend(XSocketId sock_id, const uint8_t* buf, uint32_t size) {
    XSocketHandler* xsh;
    if ((sock_id == INVALID_XSOCKET_ID) || !(xsh = reinterpret_cast<XSocketHandler*>(sock_id)) || (!buf)) {
        return XResultCode::FromError(XErrC::InvalidArgument);
    }

    return StreamSend(xsh->sock_id, buf, size);
}

XResult<uint32_t> XSocketRecvFrom(XSocketId sock_id, XSocketId & src_fd, uint8_t* buf, uint32_t size, int32_t timeout_ms /*= core::WaitType::WaitForever*/) {
    XSocketHandler* xsh;
    if ((sock_id == INVALID_XSOCKET_ID) || !(xsh = reinterpret_cast<XSocketHandler*>(sock_id))) {
        return XResult<uint32_t>::FromError(XErrC::InvalidArgument);
    }

    return UdpRecv(xsh, src_fd, buf, size, timeout_ms);
}

XResultCode XSocketSendTo(XSocketId sock_id, XSocketId dst_fd, const uint8_t* buf, uint32_t size) {
    XSocketHandler* xsh;
    if ((sock_id == INVALID_XSOCKET_ID) || (dst_fd == INVALID_XSOCKET_ID) || !(xsh = reinterpret_cast<XSocketHandler*>(sock_id)) || (!buf)) {
        return XResultCode::FromError(XErrC::InvalidArgument);
    }

    struct sockaddr_in dst_addr = {0};
    *((XSocketId*)&dst_addr) = dst_fd;
    return UdpSend(xsh->sock_id, dst_addr, buf, size);
}

XResultCode XSocketSetOpt(XSocketId sock_id, const XSocketCmdParam& param) {
    return XResultCode::FromError(XErrC::NotImplementation);
}

XResultCode XSocketGetOpt(XSocketId sock_id, XSocketCmdParam& param) {
    return XResultCode::FromError(XErrC::NotImplementation);
}

XSocketId XSocketMakeDstSocketId(const std::string& url, int32_t port) {
    struct sockaddr_in addr = Url2Addr(url, port);
    return  *((XSocketId*)&addr);
}

} //namespace shmipc::com::internal
